package edu.spangler.dang.FriendTracker;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

import edu.spangler.dang.FriendTracker.provider.Friends;

import android.app.Service;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.provider.ContactsContract;
import android.util.Log;
import android.widget.Toast;

/**
 * The main service for the Friend Tracker application.
 * It periodically updates the Friends content provider to track the location of contacts.
 * @author Paul Spangler
 */

public class FriendTracker extends Service
{
    /**
     * The name of the Intent extra for giving the user name to the service.
     */
    
    public static final String ExtraUserName = "edu.spangler.dang.UserName";

    /**
     * The intent string sent when friends' locations are updated.
     */
    
    public static final String UpdateIntent = "edu.spangler.dang.FriendsUpdated";

    /**
     * How often in milliseconds to update the friends' locations. 
     */
    
    private static final int UpdateInterval = 30000;
    
    private static boolean _running = false;
    private static IRunningChangedListener _runningChanged;
    
    private Timer _timer;
    private byte[] _user;
    private Handler _handler;
    private NetworkTask _task;
    private Uri _userContentUri;
    private long _userContentId;

    /**
     * Sets the event listener for the RunningChanged event.
     * @param listener The listener to receive the event.
     */
    
    public static void SetRunningChangedListener(IRunningChangedListener listener)
    {
        _runningChanged = listener;
    }

    /**
     * Gets whether or not the service is running.
     * @return True if the service is running, false if not.
     */
    
    public static boolean GetIsRunning()
    {
        return _running;
    }
    
    /**
     * Sets whether or not the service is running.
     * @param isRunning True if the service is running, false if not.
     */
    
    private static void SetIsRunning(boolean isRunning)
    {
        if (_running != isRunning)
        {
            _running = isRunning;
            
            if (_runningChanged != null)
            {
                _runningChanged.OnRunningChanged(isRunning);
            }
        }
    }

    /**
     * Called by the system every time a client explicitly starts the service by calling startService(Intent),
     * providing the arguments it supplied and a unique integer token representing the start request.
     * Do not call this method directly.
     * @param intent The Intent supplied to startService(Intent), as given.
     * @param flags Additional data about this start request. Currently either 0, START_FLAG_REDELIVERY, or START_FLAG_RETRY.
     * @param startId A unique integer representing this specific request to start. Use with stopSelfResult(int).
     * @return The return value indicates what semantics the system should use for the service's current started state.
     *         It may be one of the constants associated with the START_CONTINUATION_MASK bits.
     */
    
    @Override
    public int onStartCommand(final Intent intent, int flags, int startId)
    {
        if (_task == null)
        {
            String name = intent.getStringExtra(ExtraUserName);
            
            ContentValues values = new ContentValues();
            values.put(Friends.Name, name);
            values.put(Friends.Latitude, 0.0);
            values.put(Friends.Longitude, 0.0);
            
            getContentResolver().delete(Friends.ContentUri, null, null);
            _userContentUri = getContentResolver().insert(Friends.ContentUri, values);
            _userContentId = ContentUris.parseId(_userContentUri);
            
            _task = new NetworkTask();
            _user = name.getBytes();
            _timer.schedule(_task, 0, UpdateInterval);
        }
        
        return START_STICKY;
    }

    /**
     * Called by the system when the service is first created. Do not call this method directly.
     */

    @Override
    public void onCreate()
    {
        SetIsRunning(true);
        _timer = new Timer();
        _handler = new Handler();        
        Toast.makeText(getApplication(), "Friend Tracker started", Toast.LENGTH_SHORT).show();
    }

    /**
     * Called by the system to notify a Service that it is no longer used and is being removed.
     */
    
    @Override
    public void onDestroy()
    {
        _timer.cancel();
        _task.CloseConnection();
        Toast.makeText(getApplication(), "Friend Tracker stopped", Toast.LENGTH_SHORT).show();
        SetIsRunning(false);
    }

    /**
     * This service does not support binding.
     * @param intent Ignored.
     * @return Null.
     */
    
    @Override
    public IBinder onBind(Intent intent)
    {
        return null;
    }

    /**
     * An inner class to implement the timer ticks.
     */
    
    private class NetworkTask extends TimerTask implements LocationListener
    {
        /**
         * The minimum time in milliseconds to update the user's location.
         */
        
        private static final int GpsInterval = 0;

        /**
         * The server information.
         */
        
        private static final int Port = 23433;
        private static final String Host = "10.0.2.2";
        
        private static final byte MessageUpdate = '0';
        private static final byte MessageFriends = '1';
        private static final byte MessageLeave = '3';
        
        private final String[] _contactsProjection = new String[] { ContactsContract.Contacts.DISPLAY_NAME };
        
        private final Object lock = new Object();
        
        private Socket _socket;
        private InputStream _input;
        private OutputStream _output;

        /**
         * Closes the connection being used by the task.
         */
        
        public void CloseConnection()
        {
            synchronized (lock)
            {
                LocationManager manager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
                manager.removeUpdates(this);
                
                if (_socket != null && _socket.isConnected())
                {
                    try
                    {
                        try
                        {
                            _output.write(MessageLeave);
                            _output.write('\n');
                        }
                        finally
                        {
                            _input.close();
                            _output.close();
                            _socket.close();
                        }
                    }
                    catch (IOException ex)
                    {
                        // Nothing can be done about it.
                    }
                    
                    _socket = null;
                }
            }
        }

        /**
         * The action to be performed by this timer task.
         */
        
        @Override
        public void run()
        {
            synchronized (lock)
            {
                if (_socket == null)
                {
                    // Initialize the socket.
                    
                    if (!OpenConnection())
                    {
                        stopSelf();
                    }

                    LocationManager manager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
                    manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, GpsInterval, 0, this, _handler.getLooper());
                    UpdateLocation(manager.getLastKnownLocation(LocationManager.GPS_PROVIDER));
                }
                
                // Update the friends' locations every tick.
                
                UpdateFriends();
            }
        }

        /**
         * Updates the locations of every friend in the contacts list.
         */
        
        private void UpdateFriends()
        {
            String[] friends = GetFriends();
            
            if (friends != null)
            {
                try
                {
                    _output.write(MessageFriends);
                    
                    for (String friend : friends)
                    {
                        _output.write(friend.getBytes());
                        _output.write('\t');
                    }
                    
                    _output.write('\n');

                    byte[] data = new byte[LocationConverter.NumBytes];
                    ArrayList<ContentValues> values = new ArrayList<ContentValues>(friends.length);
                    
                    for (String friend : friends)
                    {
                        boolean hasData = _input.read() == 1;
                        
                        if (_input.read(data) != data.length)
                        {
                            throw new IOException("Unexpected end of data");
                        }
    
                        if (hasData)
                        {
                            Location location = LocationConverter.BytesToLocation(data, LocationManager.GPS_PROVIDER);
                            ContentValues value = new ContentValues();
                            value.put(Friends.Name, friend);
                            value.put(Friends.Latitude, location.getLatitude());
                            value.put(Friends.Longitude, location.getLongitude());
                            values.add(value);
                            
                            Log.d("FriendTracker", friend + " is at location " + location.getLatitude() + ", " + location.getLongitude());
                        }
                        else
                        {
                            Log.d("FriendTracker", friend + " has no data");
                        }
                        
                        int value;
                        
                        do
                        {
                            value = _input.read();
                            
                            if (value == -1)
                            {
                                throw new IOException("Unexpected end of name");
                            }
                        }
                        while (value != '\t');
                    }
                    
                    // Consume the closing newline.
                    // noinspection ResultOfMethodCallIgnored
                        
                    _input.read();
                    
                    // Send the updated values to the content provider.
                    
                    getContentResolver().delete(Friends.ContentUri, Friends._ID + "!=" + _userContentId, null);
                    getContentResolver().bulkInsert(Friends.ContentUri, values.toArray(new ContentValues[values.size()]));
                    
                    sendBroadcast(new Intent(UpdateIntent));
                    
                }
                catch (Exception ex)
                {
                    _handler.post(new SendToast("Friend Tracker: error getting friend locations, " + ex.toString(), Toast.LENGTH_LONG));
                }
            }
        }

        /**
         * Gets a list of friends contained in the contacts list.
         * @return The names of the friends.
         */
        
        private String[] GetFriends()
        {
            Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, _contactsProjection, null, null, null);
            
            if (!cursor.moveToFirst())
            {
                cursor.close();
                return null;
            }
            
            int x = 0;
            int nameColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
            String[] friends = new String[cursor.getCount()];
            
            do
            {
                friends[x] = cursor.getString(nameColumn);
                ++x;
            }
            while (cursor.moveToNext());
            
            cursor.close();
            
            return friends;
        }

        /**
         * Updates the location of the user.
         * @param location The user's new location.
         */
        
        private void UpdateLocation(Location location)
        {
            if (location != null)
            {
                try
                {
                    // Update the content provider.
                
                    ContentValues values = new ContentValues();
                    values.put(Friends.Latitude, location.getLatitude());
                    values.put(Friends.Longitude, location.getLongitude());
                    getContentResolver().update(_userContentUri, values, null, null);
                    
                    _output.write(MessageUpdate);
                    _output.write(LocationConverter.LocationToBytes(location));
                    _output.write(_user);
                    _output.write('\n');
                    
                    if (_input.read() != 1)
                    {
                        Log.e("FriendTracker", "Update message failed at server.");
                    }
                    
                    // Consume the closing newline.
                    // noinspection ResultOfMethodCallIgnored
                    
                    _input.read();
                }
                catch (Exception ex)
                {
                    ex.printStackTrace();
                    _handler.post(new SendToast("Friend Tracker: error updating location, " + ex.toString(), Toast.LENGTH_LONG));
                }
                
                sendBroadcast(new Intent(UpdateIntent));
            }
        }

        /**
         * Opens the connection to the server.
         * @return True if the connection was successful, false otherwise.
         */

        private boolean OpenConnection()
        {
            try
            {
                _socket = new Socket(Host, Port);
                _input = _socket.getInputStream();
                _output = _socket.getOutputStream();
                
                _output.write(_user);
                _output.write('\n');
                
                if (_input.read() != 1)
                {
                    _handler.post(new SendToast("Friend Tracker: invalid user name, " + new String(_user), Toast.LENGTH_LONG));
                    _timer.cancel();
                    return false;
                }
                
                // Consume the closing newline.
                // noinspection ResultOfMethodCallIgnored

                _input.read();
            }
            catch (IOException ex)
            {
                _handler.post(new SendToast("Friend Tracker: unable to connect to server, " + ex.toString(), Toast.LENGTH_LONG));
                _timer.cancel();
                return false;
            }
            
            return true;
        }

        /**
         * Called when the location has changed.
         * @param location The new location, as a Location object.
         */

        public void onLocationChanged(Location location)
        {
            synchronized (lock)
            {
                if (_socket != null)
                {
                    UpdateLocation(location);
                }
            }
        }

        public void onStatusChanged(String s, int i, Bundle bundle)
        { }

        public void onProviderEnabled(String s)
        { }

        public void onProviderDisabled(String s)
        { }
    }

    /**
     * An inner class to make it easier to post a toast on the main thread.
     */
    
    private class SendToast implements Runnable
    {
        private final String _message;
        private final int _duration;

        /**
         * Creates a new SendToast.
         * @param message The message to send.
         * @param duration The toast duration, one of Toast.LENGTH_SHORT or Toast.LENGTH_LONG.
         */
        
        public SendToast(String message, int duration)
        {
            _message = message;
            _duration = duration;
        }

        /**
         * Called on the other thread.
         */
        
        public void run()
        {
            Toast.makeText(getApplication(), _message, _duration).show(); 
        }
    }
}
